package elizascript;

import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;

/**
 * This class handles parsing of the script file
 * 
 * The results of this parsing and stored back in the ScriptInterop class,
 *  which this class has a reference to
 *  
 * @author Adam
 *
 */
public class ScriptInterpreter {
	private ScriptInterop script;
	private KeywordObject activeKeyword = null;
	private int KeywordID = 0;
	
	public ScriptInterpreter(ScriptInterop script)
	{
		this.script = script;
	}
	
	public boolean interpretScript(String File)
	{
		// Interpreting the script file requires 2 passes
		// The first reads the file and adds values to the appropriate arrays
		// The second creates the synonym keywords from information in the script.
		
		BufferedReader fstream = null;
		try 
		{
			// Open the file
			fstream = new BufferedReader(new FileReader(File));
			
			String line;
			
			while ((line = fstream.readLine()) != null) 
			{
				// Strip preceding and following whitespace since our script 
				// isn't sensitive to whitespace at the beginning and end of lines
				line = line.trim();
				if(line.length() == 0)
				{
					// if blank line, skip
					continue;
				}
				if(!interpretLine(line))
				{
					return false;
				}
			}
		
			// Second pass, handle synonyms
			processSynonyms();
		}
		
		// A whole load of exceptions
		catch (FileNotFoundException e) 
		{
			System.out.println("Unable to open " + File);
			return false;
		}
		catch (IOException e) 
		{
			System.out.println("IOException reading script file " + File);
			e.printStackTrace();
		}
		finally
		{
			if(fstream != null)
			{
				try {
					fstream.close();
				} catch (IOException e) {
					// Any reason which causes this exception to be thrown is way beyond our control
					// Eg full disk.
				}
			}
		}
		return true;
	}
	
	public boolean interpretLine(String line)
	{
		if(line.length() < 4)
		{
			// Script file commands are 4 letters therefore the minimum required length of a useful line is 4.
			// If it isn't zero, but is less than 4, something is wrong in the file.
			System.out.println("Invalid line in file, skipping");
			return true;
		}
		
		String[] parts = line.split("\\s");
		
		if(parts[0].length() != 4)
		{
			System.out.println("Invalid command in file (" + parts[0] + "), skipping");
			return true;
		}
		
		// Welcome Message
		if(parts[0].equals("WELC"))
		{
			if(parts.length > 1)
			{
				script.WelcomeMessages.add(joinSelectParts(parts, " ", 1));
			}
		}
		// Generic Message
		if(parts[0].equals("GMES"))
		{
			if(parts.length > 1)
			{
				script.GenericMessages.add(joinSelectParts(parts, " ", 1));
			}
		}
		// Quit Message
		else if(parts[0].equals("QMES"))
		{
			if(parts.length > 1)
			{
				script.QuitMessages.add(joinSelectParts(parts, " ", 1));
			}
		}
		// Keyword
		else if(parts[0].equals("KEYW"))
		{
			KeywordObject synonym = null;
			// If we are in the middle of a keyword, keyword won't be null.
			if(activeKeyword != null)
			{
				// If the keyword before has no decomposition rules, it is part of our synonym rule syntax.
				if(activeKeyword.DecompositionRules.size() == 0)
				{
					// We want to add this synonym (and others inside it) to the synonyms arraylist
					synonym = activeKeyword;
				}
				else
				{
					// Else the keyword is standalone, and should be added to the list of keywords.
					script.Keywords.add(activeKeyword);
				}
			}
			activeKeyword = new KeywordObject();
			
			// If we have a synonym, import their children & it into this keyword's list
			if(synonym != null)
			{
				activeKeyword.Synonyms.addAll(synonym.Synonyms);
				activeKeyword.Synonyms.add(synonym);
			}
			// Keywords use the syntax "KEYW word". There should only be 2 parts, one space in the keyword.
			if(parts.length > 2)
			{
				System.out.println("Unrecoverable error. Invalid keyword in file. Keywords should not contain any spaces");
				return false;
			}
			activeKeyword.Keyword = parts[1];
			activeKeyword.ID = KeywordID;
			KeywordID++;
		}
		// Decomposition Rule
		else if(parts[0].equals("DECR"))
		{
			if(activeKeyword == null)
			{
				System.out.println("Unexpectedly encountered a decomposition rule. Unrecoverable. " + line);
				return false;
			}
			DecompositionRule rule = new DecompositionRule();
			
			// We want to convert the decomposition rule to lower case to make comparing more straight forward.
			rule.DecompositionRule = joinSelectParts(parts, " ", 1).toLowerCase();
			activeKeyword.DecompositionRules.add(rule);
		}
		// Assembly Rule
		else if(parts[0].equals("ASSR"))
		{
			// short-circuiting allows this to not hit a nullpointerexception
			if(activeKeyword == null || activeKeyword.DecompositionRules.size() == 0)
			{
				System.out.println("Unexpectedly encountered an assembly rule. Unrecoverable. " + line);
				return false;
			}
			// Get the last decomposition rule in the keyword (this is the one which the ASSR applies to.
			DecompositionRule rule = activeKeyword.DecompositionRules.get(activeKeyword.DecompositionRules.size() - 1);
			
			if(rule.isCommand())
			{
				System.out.println("Script Error. Encountered assembly rule however the decomposition rule is for a command.");
				return false;
			}
			rule.AssemblyRules.add(joinSelectParts(parts, " ", 1));
			rule.rule_type = 2;
		}
		// Command Rule
		else if(parts[0].equals("COMR"))
		{
			// short-circuiting allows this to not hit a nullpointerexception
			if(activeKeyword == null || activeKeyword.DecompositionRules.size() == 0)
			{
				System.out.println("Unexpectedly encountered a command rule. Unrecoverable. " + line);
				return false;
			}
			// Get the last decomposition rule in the keyword (this is the one which the COMR applies to.
			DecompositionRule rule = activeKeyword.DecompositionRules.get(activeKeyword.DecompositionRules.size() - 1);
			
			if(rule.isResponseRule())
			{
				System.out.println("Script Error. Encountered command rule however the decomposition rule is for speech.");
				return false;
			}
			if(parts.length == 2)
			{
				rule.Command = parts[1];
				rule.rule_type = 1;
			}
		}
		else if(parts[0].equals("PRES") || parts[0].equals("POST"))
		{
			// minimum compatible parts : "PRES MATCH = REPLACE" = 4 parts
			if(parts.length >= 4)
			{
				String[] replaceparts = line.substring(5).split("\\s=\\s");
				if(replaceparts.length != 2)
				{
					System.out.println("Script Error. Unexpected characters found in " + parts[0] + " Substitution Rule.");
					return false;
				}
				SubstitutionRule rule = new SubstitutionRule(replaceparts[0], replaceparts[1]);
				if(parts[0].equals("PRES"))
				{
					script.PresubstitutionRules.add(rule);
				}
				else if(parts[0].equals("POST"))
				{
					script.PostsubstitutionRules.add(rule);
				}
			}
		}
		return true;
	}
	
	public void processSynonyms()
	{
		// Since you can't iterate over and add objects to an arraylist, we have a temporary array.
		ArrayList<KeywordObject> objectsToAdd = new ArrayList<KeywordObject>();
		for(KeywordObject keyword : script.Keywords)
		{
			if(keyword.Synonyms.size() > 0)
			{
				for(KeywordObject synonym : keyword.Synonyms)
				{
					// For each decomposition rule, replace the original keyword with the synonym word and add to the synonym object.
					for(DecompositionRule rule : keyword.DecompositionRules)
					{
						// Java's "Copy constructor"
						DecompositionRule newrule = (DecompositionRule) rule.clone();
						
						// Match (case-insensitively) the original keyword (surrounded by things which are letters)
						// then replace that with the synonym keyword.
						newrule.DecompositionRule = newrule.DecompositionRule.replaceAll("(?i)(?<=\\b)" + keyword.Keyword + "(?=\\b)", synonym.Keyword);
						synonym.DecompositionRules.add(newrule);
					}
					objectsToAdd.add(synonym);
				}
			}
		}
		script.Keywords.addAll(objectsToAdd);
	}
	
	// Joins all parts of the array after the element listed in the parameter begin.
	private static String joinSelectParts(String[] array, String glue, int begin) throws ArrayIndexOutOfBoundsException
	{
		return joinSelectParts(array, glue, begin, array.length - begin);
	}
	// Join certain parts of an array of strings together, using the glue string in between.
	private static String joinSelectParts(String[] array, String glue, int begin, int count) throws ArrayIndexOutOfBoundsException
	{
		if(array.length >= begin+count)
		{
			String result = "";
			for(int i=begin; i < begin + count; i++)
			{
				result += array[i] + glue;
			}
			
			return result.substring(0, result.length() - glue.length());
		}
		else
		{
			throw new ArrayIndexOutOfBoundsException();
		}
	}
}
